﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace DataServiceProvider
{
    using System;
    using System.Collections.Generic;
    using System.Data.Services.Providers;
    using System.Diagnostics;
    using System.Linq;

    internal class DSPMetadataModelExtension : IDataServiceMetadataProvider
    {
        public class ResourceTypeExtension
        {
            public string ResourceTypeName { get; set; }
            public string ExtensionPropertyName { get; set; }
            public string SourceProperty { get; set; }
            public string TargetResourceSet { get; set; }
            public string TargetProperty { get; set; }
        }

        public class ExtendedResourcePropertyAnnotation
        {
            public ResourceSet TargetResourceSet { get; set; }
            public ResourceType TargetResourceType { get; set; }
            public ResourceProperty TargetProperty { get; set; }
            public ResourceProperty SourceProperty { get; set; }
        }

        public class ResourceTypeAnnotation
        {
            public ResourceType OldResourceType { get; set; }
            public ResourceTypeExtension Extension { get; set; }
        }

        public class ResourceSetAnnotation
        {
            public ResourceSet OldResourceSet { get; set; }
        }

        public class ResourcePropertyAnnotation
        {
            public ResourceProperty OldResourceProperty { get; set; }
        }

        private IDataServiceMetadataProvider underlyingMetadataProvider;

        /// <summary>List of resource sets. Dictionary where key is the name of the resource set and value is the resource set itself.</summary>
        /// <remarks>Note that we store this such that we can quickly lookup a resource set based on its name.</remarks>
        private Dictionary<string, ResourceSet> resourceSets;

        /// <summary>List of resource types. Dictionary where key is the full name of the resource type and value is the resource type itself.</summary>
        /// <remarks>Note that we store this such that we can quickly lookup a resource type based on its name.</remarks>
        private Dictionary<string, ResourceType> resourceTypes;

        private List<ResourceTypeExtension> extensions;

        private Dictionary<ResourceType, ResourceType> oldToNewResourceTypeMapping;
        private Dictionary<ResourceSet, ResourceSet> oldToNewResourceSetMapping;
        private Dictionary<ResourceProperty, ResourceProperty> oldToNewResourcePropertyMapping;

        public DSPMetadataModelExtension(IDataServiceMetadataProvider underlyingMetadataProvider)
        {
            this.underlyingMetadataProvider = underlyingMetadataProvider;
            this.extensions = new List<ResourceTypeExtension>();
        }

        public void AddExtension(string modelExtension)
        {
            string source, target, propertyName;
            int start = 0;
            int i = modelExtension.IndexOf(" in ");
            source = modelExtension.Substring(start, i - start);
            start = i + 4;
            i = modelExtension.IndexOf(" as ");
            target = modelExtension.Substring(start, i - start);
            start = i + 4;
            propertyName = modelExtension.Substring(start);

            string[] sourceParts = source.Split('/');
            ResourceSet sourceResourceSet;
            this.underlyingMetadataProvider.TryResolveResourceSet(sourceParts[0], out sourceResourceSet);
            string sourceProperty = sourceParts[1];

            string[] targetParts = target.Split('/');
            string targetResourceSet = targetParts[0];
            string targetProperty = targetParts[1];


            ResourceTypeExtension extension = new ResourceTypeExtension();
            extension.ResourceTypeName = sourceResourceSet.ResourceType.FullName;
            extension.ExtensionPropertyName = propertyName;
            extension.SourceProperty = sourceProperty;
            extension.TargetResourceSet = targetResourceSet;
            extension.TargetProperty = targetProperty;
            this.extensions.Add(extension);
        }

        internal void SetReadOnly()
        {
            this.oldToNewResourceSetMapping = new Dictionary<ResourceSet, ResourceSet>();
            this.oldToNewResourceTypeMapping = new Dictionary<ResourceType, ResourceType>();
            this.oldToNewResourcePropertyMapping = new Dictionary<ResourceProperty, ResourceProperty>();
            List<ResourceType> originalTypes = new List<ResourceType>(this.underlyingMetadataProvider.Types);
            List<ResourceSet> originalResourceSets = new List<ResourceSet>(this.underlyingMetadataProvider.ResourceSets);

            // Assume no type inheritance
            // First of all create all new types (no properties yet)
            foreach (var originalType in originalTypes)
            {
                if (originalType.BaseType != null)
                {
                    throw new NotSupportedException("Type inheritance is not yet supported");
                }

                ResourceType newType = new ResourceType(
                    originalType.InstanceType,
                    originalType.ResourceTypeKind,
                    null,
                    originalType.Namespace,
                    originalType.Name,
                    originalType.IsAbstract);
                newType.CanReflectOnInstanceType = originalType.CanReflectOnInstanceType;
                newType.CustomState = new ResourceTypeAnnotation() { OldResourceType = originalType };

                this.oldToNewResourceTypeMapping.Add(originalType, newType);
            }

            // Now walk the types again and recreate all properties
            foreach (var map in this.oldToNewResourceTypeMapping)
            {
                foreach (var originalProperty in map.Key.PropertiesDeclaredOnThisType)
                {
                    ResourceProperty newProperty = new ResourceProperty(
                        originalProperty.Name,
                        originalProperty.Kind,
                        this.MapResourceType(originalProperty.ResourceType));
                    newProperty.CanReflectOnInstanceTypeProperty = originalProperty.CanReflectOnInstanceTypeProperty;
                    newProperty.CustomState = new ResourcePropertyAnnotation() { OldResourceProperty = originalProperty };

                    this.oldToNewResourcePropertyMapping.Add(originalProperty, newProperty);
                    map.Value.AddProperty(newProperty);                
                }
            }

            // Now walk all resource sets and recreate those
            foreach (var originalSet in originalResourceSets)
            {
                ResourceSet newSet = new ResourceSet(originalSet.Name, this.MapResourceType(originalSet.ResourceType));
                newSet.CustomState = new ResourceSetAnnotation() { OldResourceSet = originalSet };
                this.oldToNewResourceSetMapping.Add(originalSet, newSet);
            }

            // Apply extensions
            foreach (var map in this.oldToNewResourceTypeMapping)
            {
                var extensions = this.extensions.Where(e => e.ResourceTypeName == map.Value.FullName);

                foreach (var extension in extensions)
                {
                    ((ResourceTypeAnnotation)map.Value.CustomState).Extension = extension;
                    ResourceSet targetResourceSet = this.oldToNewResourceSetMapping.Values.Single(rs => rs.Name == extension.TargetResourceSet);

                    ResourceProperty extensionProperty = new ResourceProperty(
                        extension.ExtensionPropertyName, 
                        ResourcePropertyKind.ResourceReference, 
                        this.MapResourceType(targetResourceSet.ResourceType));
                    extensionProperty.CanReflectOnInstanceTypeProperty = false;
                    extensionProperty.CustomState = new ExtendedResourcePropertyAnnotation()
                    {
                        SourceProperty = map.Value.Properties.Single(p => p.Name == extension.SourceProperty),
                        TargetResourceSet = targetResourceSet,
                        TargetResourceType = targetResourceSet.ResourceType,
                        TargetProperty = targetResourceSet.ResourceType.Properties.Single(p => p.Name == extension.TargetProperty)
                    };
                    map.Value.AddProperty(extensionProperty);
                }
            }

            // Now create the dictionaries and mark everything read-only
            this.resourceTypes = new Dictionary<string, ResourceType>();
            foreach (var map in this.oldToNewResourceTypeMapping)
            {
                map.Value.SetReadOnly();
                this.resourceTypes.Add(map.Value.FullName, map.Value);
            }

            this.resourceSets = new Dictionary<string,ResourceSet>();
            foreach (var map in this.oldToNewResourceSetMapping)
            {
                map.Value.SetReadOnly();
                this.resourceSets.Add(map.Value.Name, map.Value);
            }
        }

        private ResourceSet MapResourceSet(ResourceSet oldResourceSet)
        {
            ResourceSet newResourceSet;
            if (oldResourceSet == null) return null;
            if (this.oldToNewResourceSetMapping.TryGetValue(oldResourceSet, out newResourceSet))
            {
                return newResourceSet;
            }
            else
            {
                return oldResourceSet;
            }
        }

        private ResourceType MapResourceType(ResourceType oldResourceType)
        {
            ResourceType newResourceType;
            if (oldResourceType == null) return null;
            if (this.oldToNewResourceTypeMapping.TryGetValue(oldResourceType, out newResourceType))
            {
                return newResourceType;
            }
            else
            {
                return oldResourceType;
            }
        }

        private ResourceProperty MapResourceProperty(ResourceProperty oldResourceProperty)
        {
            ResourceProperty newResourceProperty;
            if (oldResourceProperty == null) return null;
            if (this.oldToNewResourcePropertyMapping.TryGetValue(oldResourceProperty, out newResourceProperty))
            {
                return newResourceProperty;
            }
            else
            {
                return oldResourceProperty;
            }
        }
        #region IDataServiceMetadataProvider Members

        public string ContainerName
        {
            get { return this.underlyingMetadataProvider.ContainerName; }
        }

        public string ContainerNamespace
        {
            get { return this.underlyingMetadataProvider.ContainerNamespace; }
        }

        public IEnumerable<ResourceType> GetDerivedTypes(ResourceType resourceType)
        {
            yield break;
        }

        public ResourceAssociationSet GetResourceAssociationSet(ResourceSet resourceSet, ResourceType resourceType, ResourceProperty resourceProperty)
        {
            ExtendedResourcePropertyAnnotation extension = resourceProperty.CustomState as ExtendedResourcePropertyAnnotation;
            if (extension != null)
            {
                return new ResourceAssociationSet(
                    resourceProperty.Name,
                    new ResourceAssociationSetEnd(resourceSet, resourceType, resourceProperty),
                    new ResourceAssociationSetEnd(extension.TargetResourceSet, extension.TargetResourceType, null));
            }

            var rsa = resourceSet.CustomState as ResourceSetAnnotation;
            var rta = resourceType.CustomState as ResourceTypeAnnotation;
            var rpa = resourceProperty.CustomState as ResourcePropertyAnnotation;

            ResourceAssociationSet oldResourceAssociationSet = this.underlyingMetadataProvider.GetResourceAssociationSet(
                rsa != null ? rsa.OldResourceSet : resourceSet,
                rta != null ? rta.OldResourceType : resourceType,
                rpa != null ? rpa.OldResourceProperty : resourceProperty);

            var end1 = new ResourceAssociationSetEnd(
                this.MapResourceSet(oldResourceAssociationSet.End1.ResourceSet),
                this.MapResourceType(oldResourceAssociationSet.End1.ResourceType),
                this.MapResourceProperty(oldResourceAssociationSet.End1.ResourceProperty));
            var end2 = new ResourceAssociationSetEnd(
                this.MapResourceSet(oldResourceAssociationSet.End2.ResourceSet),
                this.MapResourceType(oldResourceAssociationSet.End2.ResourceType),
                this.MapResourceProperty(oldResourceAssociationSet.End2.ResourceProperty));

            return new ResourceAssociationSet(oldResourceAssociationSet.Name, end1, end2);
        }

        public bool HasDerivedTypes(ResourceType resourceType)
        {
            return false;
        }

        public IEnumerable<ResourceSet> ResourceSets
        {
            get { return this.resourceSets.Values; }
        }

        public IEnumerable<ServiceOperation> ServiceOperations
        {
            get { yield break; }
        }

        public bool TryResolveResourceSet(string name, out ResourceSet resourceSet)
        {
            return this.resourceSets.TryGetValue(name, out resourceSet);
        }

        public bool TryResolveResourceType(string name, out ResourceType resourceType)
        {
            return this.resourceTypes.TryGetValue(name, out resourceType);
        }

        public bool TryResolveServiceOperation(string name, out ServiceOperation serviceOperation)
        {
            serviceOperation = null;
            return false;
        }

        public IEnumerable<ResourceType> Types
        {
            get { return this.resourceTypes.Values; }
        }

        #endregion
    }
}